/*
 *
 *  * Unidata Platform
 *  * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *  *
 *  * Commercial License
 *  * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *  *
 *  * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 *  * For clarification or additional options, please contact: info@unidata-platform.com
 *  * -------
 *  * Disclaimer:
 *  * -------
 *  * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 *  * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 *  * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 *  * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 *  * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 *
 */
package org.unidata.mdm.rest.v1.dq.core.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.dq.core.configuration.DataQualityDescriptors;
import org.unidata.mdm.dq.core.context.CleanseFunctionContext;
import org.unidata.mdm.dq.core.context.GetQualityModelContext;
import org.unidata.mdm.dq.core.context.UpsertQualityModelContext;
import org.unidata.mdm.dq.core.dto.GetFunctionsModelResult;
import org.unidata.mdm.dq.core.dto.GetQualityModelResult;
import org.unidata.mdm.dq.core.exception.DataQualityRuntimeException;
import org.unidata.mdm.dq.core.service.impl.CleanseFunctionCacheComponent;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionLibrary;
import org.unidata.mdm.dq.core.type.model.instance.CleanseFunctionElement;
import org.unidata.mdm.dq.core.type.model.source.AbstractCleanseFunctionSource;
import org.unidata.mdm.dq.core.util.DQUtils;
import org.unidata.mdm.rest.system.ro.DetailedErrorResponseRO;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.rest.v1.dq.core.converter.CleanseFunctionConverter;
import org.unidata.mdm.rest.v1.dq.core.converter.CleanseFunctionGroupsConverter;
import org.unidata.mdm.rest.v1.dq.core.converter.ExecuteFunctionRequestConverter;
import org.unidata.mdm.rest.v1.dq.core.converter.ExecuteFunctionResponseConverter;
import org.unidata.mdm.rest.v1.dq.core.exception.DataQualityRestExceptionIds;
import org.unidata.mdm.rest.v1.dq.core.ro.CleanseFunctionResultRO;
import org.unidata.mdm.rest.v1.dq.core.ro.ExecuteFunctionRequestRO;
import org.unidata.mdm.rest.v1.dq.core.ro.ExecuteFunctionResultRO;
import org.unidata.mdm.rest.v1.dq.core.ro.GetCleanseFunctionGroupsRO;
import org.unidata.mdm.rest.v1.dq.core.ro.GetCleanseFunctionRO;
import org.unidata.mdm.rest.v1.dq.core.ro.SuggestCleanseFunctionRO;
import org.unidata.mdm.rest.v1.dq.core.ro.UpsertCleanseFunctionGroupsRO;
import org.unidata.mdm.rest.v1.dq.core.ro.UpsertCleanseFunctionRO;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * The Class CleanseFunctionRestService.
 *
 * @author Michael Yashin. Created on 19.05.2015.
 */
@Path(CleanseFunctionRestService.SERVICE_PATH)
@Consumes({ "application/json" })
@Produces({ "application/json" })
public class CleanseFunctionRestService extends AbstractRestService {
    /**
     * Service path.
     */
    public static final String SERVICE_PATH = "cleanse-functions";
    /**
     * Tag.
     */
    public static final String SERVICE_TAG = "cleanse-functions";
    /**
     * MMS instance.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * The cache component.
     */
    @Autowired
    private CleanseFunctionCacheComponent cleanseFunctionCacheComponent;
    /**
     * Groups converter.
     */
    private CleanseFunctionGroupsConverter cleanseFunctionGroupsConverter = new CleanseFunctionGroupsConverter();
    /**
     * FN converter.
     */
    private CleanseFunctionConverter cleanseFunctionConverter = new CleanseFunctionConverter();
    /**
     * Execute FN request converter.
     */
    private ExecuteFunctionRequestConverter executeFunctionRequestConverter = new ExecuteFunctionRequestConverter();
    /**
     * Execute response converter.
     */
    private ExecuteFunctionResponseConverter executeFunctionResponseConverter = new ExecuteFunctionResponseConverter();
    /**
     * Gets all groups.
     * @param draftId the draft id
     * @return list of groups with functions
     */
    @GET
    @Path("/groups")
    @Operation(
            description = "Return all (filled) function groups, starting from the root.",
            method = HttpMethod.GET,
            tags = SERVICE_TAG,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = GetCleanseFunctionGroupsRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response findAll(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId) {

        GetQualityModelResult get = metaModelService.get(GetQualityModelContext.builder()
                .allFunctionGroups(true)
                .draftId(draftId)
                .build());

        GetCleanseFunctionGroupsRO result = new GetCleanseFunctionGroupsRO();
        result.setRoot(cleanseFunctionGroupsConverter.to(get.getFunctionGroups()));

        return ok(result);
    }
    /**
     * Updates groups tree
     * @param draftId
     * @param groups
     * @return
     */
    @POST
    @Path("/groups")
    @Operation(
            description = "Upserts new cleanse function groups structure.",
            method = HttpMethod.POST,
            tags = SERVICE_TAG,
            requestBody = @RequestBody(
                    content = @Content(schema = @Schema(implementation = UpsertCleanseFunctionGroupsRO.class)),
                    description = "Cleanse function groups structure."),
            responses = {
                @ApiResponse(content = @Content(schema = @Schema(implementation = CleanseFunctionResultRO.class)), responseCode = "200"),
                @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
            }
        )
    public Response update(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            UpsertCleanseFunctionGroupsRO groups) {

        metaModelService.upsert(UpsertQualityModelContext.builder()
            .groupsUpdate(cleanseFunctionGroupsConverter.from(groups.getRoot()))
            .draftId(draftId)
            .build());

        return ok(new CleanseFunctionResultRO(true));
    }

    /**
     * Gets function by id.
     *
     * @param id
     *            the id
     * @return the by id
     * @throws Exception
     *             the exception
     */
    @GET
    @Path("{id}")
    @Operation(
        description = "Gets a specific cleanse function by ID",
        method = HttpMethod.GET,
        tags = SERVICE_TAG,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = GetCleanseFunctionRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response get(
            @Parameter(description = "Id", in = ParameterIn.PATH) @PathParam("id") String id,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId) {

        GetQualityModelResult get = metaModelService.get(GetQualityModelContext.builder()
            .cleanseFunctionIds(Collections.singletonList(id))
            .draftId(draftId)
            .build());

        GetCleanseFunctionRO result = new GetCleanseFunctionRO();
        GetFunctionsModelResult gfr = get.getFunctions();
        if (CollectionUtils.isNotEmpty(gfr.getFunctions()) && gfr.getFunctions().size() == 1) {
            result.setFunction(cleanseFunctionConverter.to(gfr.getFunctions().get(0)));
        }

        return ok(result);
    }
    /**
     * Creates a new cleanse function definition.
     * @param upsert the payload
     * @return state
     */
    @POST
    @Operation(
        description = "Creates a new cleanse function definition.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG,
        requestBody = @RequestBody(
                content = @Content(schema = @Schema(implementation = UpsertCleanseFunctionRO.class)),
                description = "Cleanse function definition."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = GetCleanseFunctionRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response create(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            UpsertCleanseFunctionRO upsert) {

        AbstractCleanseFunctionSource<?> target = cleanseFunctionConverter.from(upsert.getFunction());
        metaModelService.upsert(UpsertQualityModelContext.builder()
                .functionsUpdate(target)
                .draftId(draftId)
                .build());

        return get(target.getName(), draftId);
    }
    /**
     * Updates an existing cleanse function definition.
     * @param upsert the payload
     * @return state
     */
    @PUT
    @Path("{id}")
    @Operation(
        description = "Updates an existing cleanse function definition.",
        method = HttpMethod.PUT,
        tags = SERVICE_TAG,
        requestBody = @RequestBody(
                content = @Content(schema = @Schema(implementation = UpsertCleanseFunctionRO.class)),
                description = "Cleanse function definition."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = GetCleanseFunctionRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response update(
            @Parameter(description = "Function ID.", in = ParameterIn.PATH) @PathParam("id") String id,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            UpsertCleanseFunctionRO upsert) {

        AbstractCleanseFunctionSource<?> target = cleanseFunctionConverter.from(upsert.getFunction());
        metaModelService.upsert(UpsertQualityModelContext.builder()
                .functionsUpdate(target)
                .functionsDelete(!StringUtils.equals(id, target.getName()) ? id : null)
                .draftId(draftId)
                .build());

        return get(target.getName(), draftId);
    }
    /**
     * Deletes an existing cleanse function defibition.
     *
     * @param id the function id
     * @return state
     */
    @DELETE
    @Path("{id}")
    @Operation(
        description = "Deletes an existing cleanse function defibition by ID.",
        method = HttpMethod.DELETE,
        tags = SERVICE_TAG,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = CleanseFunctionResultRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response delete(
            @Parameter(description = "Function ID.", in = ParameterIn.PATH) @PathParam("id") String id,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId) {

        metaModelService.upsert(UpsertQualityModelContext.builder()
                .functionsDelete(id)
                .draftId(draftId)
                .build());

        return ok(new CleanseFunctionResultRO(true));
    }
    /**
     * Execute function (without params validation)
     *
     * @param request the request
     * @return the response
     * @throws Exception the wrong params exception
     */
    @POST
    @Path("/execute")
    @Operation(
        description = "Runs a function using supplied parameters.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ExecuteFunctionRequestRO.class)), description = "Function input."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = ExecuteFunctionResultRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "401"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response execute(
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") Long draftId,
            ExecuteFunctionRequestRO request) {

        CleanseFunctionElement cfe = metaModelService.instance(
                DataQualityDescriptors.DQ, draftId, SecurityUtils.getCurrentUserStorageId(), DQUtils.DEFAULT_MODEL_INSTANCE_ID)
               .getFunction(request.getFunctionName());

        if (Objects.isNull(cfe)) {
            throw new DataQualityRuntimeException("Cleanse function [{}] not found.",
                    DataQualityRestExceptionIds.EX_DQ_EXECUTE_FUNCTION_NOT_FOUND,
                    request.getFunctionName());
        }

        CleanseFunctionContext cfc = executeFunctionRequestConverter.to(request);
        return ok(executeFunctionResponseConverter.to(cfe.execute(cfc)));
    }
    /**
     * Suggests cleanse function implementation names to be used in CF definitions.
     * @param library the library name
     * @param version the library version
     * @return response
     */
    @GET
    @Path("/suggest")
    @Operation(
        description = "Suggests cleanse function implementation from a given library content.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = SuggestCleanseFunctionRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500")
        }
    )
    public Response suggest(
                @Parameter(description = "Library name.") @QueryParam("library") String library,
                @Parameter(description = "Library version.") @QueryParam("version") String version) {

        CleanseFunctionLibrary cfl = cleanseFunctionCacheComponent
            .find(SecurityUtils.getCurrentUserStorageId(), library, version);

        if (Objects.isNull(cfl)) {
            throw new DataQualityRuntimeException("Library [{}] version [{}] not found.",
                    DataQualityRestExceptionIds.EX_DQ_SUGGEST_LIBRARY_NOT_FOUND, library, version);
        }

        return ok(new SuggestCleanseFunctionRO(new ArrayList<>(cfl.suggest())));
    }
}
